#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019-2021 A S Lewis
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.


"""Info operation classes."""


# Import Gtk modules
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GObject


# Import other modules
import os
import queue
import re
import requests
import signal
import subprocess
import threading


# Import our modules
import __main__
import downloads
import utils
# Use same gettext translations
from mainapp import _


# Classes


class InfoManager(threading.Thread):

    """Called by mainapp.TartubeApp.info_manager_start().

    Python class to create a system child process, to do one of three jobs:

    1. Fetch a list of available formats for a video, directly from youtube-dl

    2. Fetch a list of available subtitles for a video, directly from
        youtube-dl

    3. Test youtube-dl with specified download options; everything is
        downloaded into a temporary directory

    4. Check the Tartube website, and inform the user if a new release is
        available

    Reads from the child process STDOUT and STDERR, having set up a
    downloads.PipeReader object to do so in an asynchronous way.

    Args:

        app_obj (mainapp.TartubeApp): The main application

        info_type (str): The type of information to fetch: 'formats' for a list
            of video formats, 'subs' for a list of subtitles, or 'test_ytdl'
            to test youtube-dl with specified options, 'version' to check for a
            new release of Tartube

        media_data_obj (media.Video): For 'formats' and 'subs', the media.Video
            object for which formats/subtitles should be fetched. For
            'test_ytdl', set to None

        url_string (str): For 'test_ytdl', the video URL to download (can be
            None or an empty string, if no download is required, for example
            'youtube-dl --version'. For 'formats' and 'subs', set to None

        options_string (str): For 'test_ytdl', a string containing one or more
            youtube-dl download options. The string, generated by a
            Gtk.TextView, typically contains newline and/or multiple whitespace
            characters; the info.InfoManager code deals with that. Can be None
            or an empty string, if no download options are required. For
            'formats' and 'subs', set to None

    """


    # Standard class methods


    def __init__(self, app_obj, info_type, media_data_obj, url_string,
    options_string):

        super(InfoManager, self).__init__()

        # IV list - class objects
        # -----------------------
        # The mainapp.TartubeApp object
        self.app_obj = app_obj
        # The video for which information will be fetched (None if
        #   self.info_type is 'test_ytdl')
        self.video_obj = media_data_obj

        # This object reads from the child process STDOUT and STDERR in an
        #   asynchronous way
        # Standard Python synchronised queue classes
        self.stdout_queue = queue.Queue()
        self.stderr_queue = queue.Queue()
        # The downloads.PipeReader objects created to handle reading from the
        #   pipes
        self.stdout_reader = downloads.PipeReader(self.stdout_queue)
        self.stderr_reader = downloads.PipeReader(self.stderr_queue)

        # The child process created by self.create_child_process()
        self.child_process = None


        # IV list - other
        # ---------------
        # The type of information to fetch: 'formats' for a list of video
        #   formats, 'subs' for a list of subtitles, 'test_ytdl' to test
        #   youtube-dl with specified options, or 'version' to check for a new
        #   release of Tartube
        self.info_type = info_type
        # For 'test_ytdl', the video URL to download (can be None or an empty
        #   string, if no download is required, for example
        #   'youtube-dl --version'. For 'formats' and 'subs', set to None
        self.url_string = url_string
        # For 'test_ytdl', a string containing one or more youtube-dl download
        #   options. The string, generated by a Gtk.TextView, typically
        #   contains newline and/or multiple whitespace characters; the
        #   info.InfoManager code deals with that. Can be None or an empty
        #   string, if no download options are required. For 'formats' and
        #   'subs', set to None
        self.options_string = options_string
        # For 'version', the version numbers (e.g. 1.2.003) retrieved from the
        #   main website (representing a stable release), and from github
        #   (representing a development release)
        self.stable_version = None
        self.dev_version = None

        # Flag set to True if the info operation succeeds, False if it fails
        self.success_flag = False

        # The list of formats/subtitles extracted from STDOUT
        self.output_list = []

        # (For debugging purposes, store any STDOUT/STDERR messages received;
        #   otherwise we would just set a flag if a STDERR message was
        #   received)
        self.stdout_list = []
        self.stderr_list = []


        # Code
        # ----

        # Let's get this party started!
        self.start()


    # Public class methods


    def run(self):

        """Called as a result of self.__init__().

        Creates a child process to run the youtube-dl system command.

        Reads from the child process STDOUT and STDERR, and calls the main
        application with the result of the process (success or failure).
        """

        # Checking for a new release of Tartube doesn't involve any system
        #   commands or child processes, so it is handled by a separate
        #   function
        if self.info_type == 'version':

            return self.run_check_version()

        # Show information about the info operation in the Output Tab
        if self.info_type == 'test_ytdl':

            msg = _(
                'Starting info operation, testing downloader with specified' \
                + ' options',
            )

        else:

            if self.info_type == 'formats':

                msg = _(
                    'Starting info operation, fetching list of video/audio'\
                    + ' formats for \'{0}\'',
                ).format(self.video_obj.name)

            else:

                msg = _(
                    'Starting info operation, fetching list of subtitles'\
                    + ' for \'{0}\'',
                ).format(self.video_obj.name)

        self.app_obj.main_win_obj.output_tab_write_stdout(1, msg)

        # Convert a path beginning with ~ (not on MS Windows)
        ytdl_path = self.app_obj.check_downloader(self.app_obj.ytdl_path)
        if os.name != 'nt':
            ytdl_path = re.sub('^\~', os.path.expanduser('~'), ytdl_path)

        # Prepare the system command
        if self.info_type == 'formats':

            cmd_list = [
                ytdl_path,
                '--list-formats',
                self.video_obj.source,
            ]

        elif self.info_type == 'subs':

            cmd_list = [
                ytdl_path,
                '--list-subs',
                self.video_obj.source,
            ]

        else:

            if self.app_obj.ytdl_path_custom_flag:
                cmd_list = ['python3'] + [ytdl_path]
            else:
                cmd_list = [ytdl_path]

            if self.options_string is not None \
            and self.options_string != '':

                # Parse the string into a list. It was obtained from a
                #   Gtk.TextView, so it can contain newline and/or multiple
                #   whitepsace characters. Whitespace characters within
                #   double quotes "..." must be preserved
                option_list = utils.parse_options(self.options_string)
                for item in option_list:
                    cmd_list.append(item)

            if self.url_string is not None \
            and self.url_string != '':

                cmd_list.append('-o')
                cmd_list.append(
                    os.path.join(
                        self.app_obj.temp_test_dir,
                        '%(title)s.%(ext)s',
                    ),
                )

                cmd_list.append(self.url_string)

        # Create the new child process
        self.create_child_process(cmd_list)

        # Show the system command in the Output Tab
        space = ' '
        self.app_obj.main_win_obj.output_tab_write_system_cmd(
            1,
            space.join(cmd_list),
        )

        # So that we can read from the child process STDOUT and STDERR, attach
        #   a file descriptor to the PipeReader objects
        if self.child_process is not None:

            self.stdout_reader.attach_file_descriptor(
                self.child_process.stdout,
            )

            self.stderr_reader.attach_file_descriptor(
                self.child_process.stderr,
            )

        while self.is_child_process_alive():

            # Read from the child process STDOUT, and convert into unicode for
            #   Python's convenience
            while not self.stdout_queue.empty():

                stdout = self.stdout_queue.get_nowait().rstrip()
                if stdout:

                    if os.name == 'nt':
                        stdout = stdout.decode('cp1252')
                    else:
                        stdout = stdout.decode('utf-8')

                    self.output_list.append(stdout)
                    self.stdout_list.append(stdout)

                    # Show command line output in the Output Tab
                    self.app_obj.main_win_obj.output_tab_write_stdout(
                        1,
                        stdout,
                    )

        # The child process has finished
        while not self.stderr_queue.empty():

            # Read from the child process STDERR queue (we don't need to read
            #   it in real time), and convert into unicode for python's
            #   convenience
            stderr = self.stderr_queue.get_nowait().rstrip()
            if os.name == 'nt':
                stderr = stderr.decode('cp1252')
            else:
                stderr = stderr.decode('utf-8')

            if stderr:

                # While testing youtube-dl, don't treat anything as an error
                if self.info_type == 'test_ytdl':
                    self.stdout_list.append(stderr)

                # When fetching subtitles from a video that has none, don't
                #   treat youtube-dl WARNING: messages as something that
                #   makes the info operation fail
                elif self.info_type == 'subs':

                    if not re.match('WARNING\:', stderr):
                        self.stderr_list.append(stderr)

                # When fetching formats, recognise all warnings as errors
                else:
                    self.stderr_list.append(stderr)

                # Show command line output in the Output Tab
                self.app_obj.main_win_obj.output_tab_write_stderr(
                    1,
                    stderr,
                )

        # (Generate our own error messages for debugging purposes, in certain
        #   situations)
        if self.child_process is None:

            msg = _('System process did not start')
            self.stderr_list.append(msg)
            self.app_obj.main_win_obj.output_tab_write_stdout(
                1,
                msg,
            )

        elif self.child_process.returncode > 0:

            msg = _('Child process exited with non-zero code: {}').format(
                self.child_process.returncode,
            )
            self.app_obj.main_win_obj.output_tab_write_stdout(
                1,
                msg,
            )

        # Operation complete. self.success_flag is checked by
        #   mainapp.TartubeApp.info_manager_finished()
        if not self.stderr_list:
            self.success_flag = True

        # Show a confirmation in the the Output Tab
        self.app_obj.main_win_obj.output_tab_write_stdout(
            1,
            _('Info operation finished'),
        )

        # Let the timer run for a few more seconds to prevent Gtk errors (for
        #   systems with Gtk < 3.24)
        GObject.timeout_add(
            0,
            self.app_obj.info_manager_halt_timer,
        )


    def run_check_version(self):

        """Called by self.run().

        Checking for a new release of Tartube doesn't involve any system
        commands or child processes, so it is handled separately by this
        function.

        There is a stable release at Sourceforge, and a development release at
        Github. Fetch the VERSION file from each, and store the stable/
        development versions, so that mainapp.TartubeApp.info_manager_finished
        can display them.
        """

        # Show information about the info operation in the Output Tab
        self.app_obj.main_win_obj.output_tab_write_stdout(
            1,
            _('Starting info operation, checking for new releases of Tartube'),
        )

        # Check the stable version, http://tartube.sourceforge.io/VERSION
        stable_path = __main__.__website__ + '/VERSION'

        self.app_obj.main_win_obj.output_tab_write_stdout(
            1,
            _('Checking stable release...'),
        )

        self.app_obj.main_win_obj.output_tab_write_system_cmd(1, stable_path)

        try:
            request_obj = requests.get(
                stable_path,
                timeout = self.app_obj.request_get_timeout,
            )

            response = utils.strip_whitespace(request_obj.text)
            if not re.search('^\d+\.\d+\.\d+\s*$', response):

                self.app_obj.main_win_obj.output_tab_write_stdout(
                    1,
                    _('Ignoring invalid version'),
                )

            else:

                self.stable_version = response

                self.app_obj.main_win_obj.output_tab_write_stdout(
                    1,
                    _('Retrieved version:') + ' ' + str(response),
                )

        except:

            self.app_obj.main_win_obj.output_tab_write_stdout(
                1,
                _('Connection failed'),
            )

        # Check the development version,
        #   http://raw.githubusercontent.com/axcore/tartube/master/VERSION
        dev_path = __main__.__website_dev__ + '/VERSION'

        self.app_obj.main_win_obj.output_tab_write_stdout(
            1,
            _('Checking development release...'),
        )

        self.app_obj.main_win_obj.output_tab_write_system_cmd(1, dev_path)

        try:
            request_obj = requests.get(
                dev_path,
                timeout = self.app_obj.request_get_timeout,
            )

            response = utils.strip_whitespace(request_obj.text)
            if not re.search('^\d+\.\d+\.\d+\s*$', response):

                self.app_obj.main_win_obj.output_tab_write_stdout(
                    1,
                    _('Ignoring invalid version'),
                )

            else:

                self.dev_version = response

                self.app_obj.main_win_obj.output_tab_write_stdout(
                    1,
                    _('Retrieved version:') + ' ' + str(response),
                )

        except:

            self.app_obj.main_win_obj.output_tab_write_stdout(
                1,
                _('Connection failed'),
            )

        # Operation complete. self.success_flag is checked by
        #   mainapp.TartubeApp.info_manager_finished()
        self.success_flag = True

        # Show a confirmation in the the Output Tab
        self.app_obj.main_win_obj.output_tab_write_stdout(
            1,
            _('Info operation finished'),
        )

        # Let the timer run for a few more seconds to prevent Gtk errors (for
        #   systems with Gtk < 3.24)
        GObject.timeout_add(
            0,
            self.app_obj.info_manager_halt_timer,
        )


    def create_child_process(self, cmd_list):

        """Called by self.run().

        Based on code from downloads.VideoDownloader.create_child_process().

        Executes the system command, creating a new child process which
        executes youtube-dl.

        Args:

            cmd_list (list): Python list that contains the command to execute.

        """

        info = preexec = None

        if os.name == 'nt':
            # Hide the child process window that MS Windows helpfully creates
            #   for us
            info = subprocess.STARTUPINFO()
            info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        else:
            # Make this child process the process group leader, so that we can
            #   later kill the whole process group with os.killpg
            preexec = os.setsid

        try:
            self.child_process = subprocess.Popen(
                cmd_list,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                preexec_fn=preexec,
                startupinfo=info,
            )

        except (ValueError, OSError) as error:
            # (The code in self.run() will spot that the child process did not
            #   start)
            self.stderr_list.append(_('Child process did not start'))


    def is_child_process_alive(self):

        """Called by self.run() and .stop_info_operation().

        Based on code from downloads.VideoDownloader.is_child_process_alive().

        Called continuously during the self.run() loop to check whether the
        child process has finished or not.

        Returns:

            True if the child process is alive, otherwise returns False.

        """

        if self.child_process is None:
            return False

        return self.child_process.poll() is None


    def stop_info_operation(self):

        """Called by mainapp.TartubeApp.do_shutdown(), .stop_continue(),
        .on_button_stop_operation() and mainwin.MainWin.on_stop_menu_item().

        Based on code from downloads.VideoDownloader.stop().

        Terminates the child process.
        """

        if self.is_child_process_alive():

            if os.name == 'nt':
                # os.killpg is not available on MS Windows (see
                #   https://bugs.python.org/issue5115 )
                self.child_process.kill()

                # When we kill the child process on MS Windows the return code
                #   gets set to 1, so we want to reset the return code back to
                #   0
                self.child_process.returncode = 0

            else:
                os.killpg(self.child_process.pid, signal.SIGKILL)
